iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
生成式 AI

一起來打造 PTT 文章智慧問答系統!系列 第 14

【Day 14】實作 Django REST Framework API - 建立文章 API:列表、篩選、查詢

  • 分享至 

  • xImage
  •  

Hi大家好,
這是我參加 iT 邦幫忙鐵人賽的第 1 次挑戰,這次的主題聚焦在結合 Python 爬蟲、RAG(檢索增強生成)與 AI,打造一套 PTT 文章智慧問答系統。在過程中,我會依照每天進度上傳程式碼到 GitHub ,方便大家參考學習。也歡迎留言或來信討論,我的信箱是 gerryearth@gmail.com


我們今天將開始實作第一個 API——文章列表查詢 API,並加入條件篩選、分頁、以及 API 文件生成,讓系統能夠對外穩定提供資料查詢服務,也為後續的智慧問答功能打好基礎。


今日目標

  • 建立 Article 模型的 Serializer 與 View
  • 製作 GET /api/posts/ 查詢 API
  • 支援條件篩選(看板、作者、關鍵字)
  • 簡單測試 API 功能

安裝 Django REST Framework

pip install djangorestframework

settings.py 中加入:

INSTALLED_APPS = [
    ...
    'rest_framework',
]

有了這個模組,我們就能在 views.py 建立 API,例如以下範例可以接受用戶端 GET 請求(request),並給予回應(response):

from rest_framework.views import APIView
from rest_framework.response import Response

class HelloView(APIView):
   def get(self, request):
       return Response({"message": "Hello, world!"})

註冊 API 路由

請先安裝所需套件:

pip install django-filter drf-spectacular

django-filter 在 API 或 Django views 中,讓你可以用查詢參數 (query parameters) 進行資料過濾。
drf-spectacular 可以自動產生 OpenAPI 3 格式的 API 規格文件,並且能提供互動式的 Swagger / Redoc 頁面。

並在 settings.py 加上:

INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'django_filters',
    'drf_spectacular',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

ptt_rag_dev/urls.py 建立:

from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

urlpatterns = [
    path('api/admin/', admin.site.urls),
    path('api/', include('article.urls')),
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/schema/doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]

並建立 article/urls.py

from django.urls import path
from . import views
urlpatterns = [
    path('posts/', views.ArticleListView.as_view(), name='article-list'),
]

建立 ArticleSerializer

article/serializers.py 中新增:

from rest_framework import serializers
from .models import Article

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = '__all__'

class ArticleListRequestSerializer(serializers.Serializer):
    author_name = serializers.CharField(help_text="作者名稱", write_only=True, required=False)
    board_name = serializers.CharField(help_text="看板名稱", write_only=True, required=False)
    start_date = serializers.DateField(help_text="起始日期", write_only=True, required=False)
    end_date = serializers.DateField(help_text="結束日期", write_only=True, required=False)
    limit = serializers.IntegerField(help_text="每頁返回的筆數 (預設 50)", write_only=True, default=50, min_value=1)
    offset = serializers.IntegerField(help_text="從第幾筆開始 (預設 0)", write_only=True, required=False, min_value=0)

為什麼需要 serializers?

serializers 可以使 Django 的資料模型(如:Article)透過 API 傳給前端時,主要的功能如下:

  1. 把 Python 物件轉成 JSON(序列化 serialization)
  2. 從前端傳回的 JSON 建立/更新資料模型(反序列化 deserialization + 資料驗證)

ArticleSerializer 的目的是將 Article 模型序列化 + 反序列化(含驗證):用於資料模型操作(資料輸入輸出),而 ArticleListRequestSerializer 只用來反序列化 + 驗證輸入資料(不序列化、不儲存),可以用來過濾資料、搜尋條件或分頁。


建立 article/views.py

from datetime import datetime, time
from rest_framework import status, serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.pagination import LimitOffsetPagination
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse, inline_serializer
from .models import Article
from .serializers import ArticleSerializer, ArticleListRequestSerializer
import traceback
from log_app.models import Log


class ArticleListView(APIView):
    @extend_schema(
        description="取得最新 50 篇文章,可使用 limit、offset 進行分頁,可使用作者名稱、版面、時間範圍進行過濾。",
        parameters=[
            OpenApiParameter("limit", int, OpenApiParameter.QUERY, description="每頁返回的筆數 (預設 50)"),
            OpenApiParameter("offset", int, OpenApiParameter.QUERY, description="從第幾筆開始 (預設 0)"),
            OpenApiParameter("author_name", str, OpenApiParameter.QUERY, description="篩選特定發文者的文章"),
            OpenApiParameter("board_name", str, OpenApiParameter.QUERY, description="篩選特定版面的文章"),
            OpenApiParameter("start_date", str, OpenApiParameter.QUERY, description="篩選起始日期 (YYYY-MM-DD)", ),
            OpenApiParameter("end_date", str, OpenApiParameter.QUERY, description="篩選結束日期 (YYYY-MM-DD)", ),
        ],
        responses={
            200: OpenApiResponse(
                response=inline_serializer(
                    name='ArticleListResponse',
                    fields={
                        'count': serializers.IntegerField(read_only=True),
                        'next': serializers.CharField(read_only=True),
                        'previous': serializers.CharField(read_only=True),
                        'results': ArticleSerializer(many=True, read_only=True),
                    }
                ),
            )
        },
    )
    def get(self, request):
        article_list_request_serializer = ArticleListRequestSerializer(data=request.query_params)
        if not article_list_request_serializer.is_valid():
            Log.objects.create(level='ERROR', category='user-posts', message='查詢參數不合法',
                               traceback=traceback.format_exc())
            return Response(article_list_request_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        articles = Article.objects.all()
        author_name = article_list_request_serializer.validated_data.get("author_name")
        board_name = article_list_request_serializer.validated_data.get("board_name")
        start_date = article_list_request_serializer.validated_data.get("start_date")
        end_date = article_list_request_serializer.validated_data.get("end_date")
        if author_name:
            articles = articles.filter(author=author_name)
        if board_name:
            articles = articles.filter(board=board_name)
        start_datetime = datetime.combine(start_date, time.min) if start_date else None
        end_datetime = datetime.combine(end_date, time.max) if end_date else None
        if start_datetime and end_datetime:
            articles = articles.filter(post_time__range=[start_datetime, end_datetime])
        elif start_datetime:
            articles = articles.filter(post_time__gte=start_datetime)
        elif end_datetime:
            articles = articles.filter(post_time__lte=end_datetime)
        paginator = LimitOffsetPagination()
        paginator.default_limit = 50
        paginated_queryset = paginator.paginate_queryset(articles.order_by('id'), request)
        serializer = ArticleSerializer(paginated_queryset, many=True)
        return paginator.get_paginated_response(serializer.data)

以下簡單說明此 API 的重點功能:

類別與套件

  • APIView:DRF 提供的基礎視圖類別,用來建立 API 端點。
  • extend_schema:由 drf-spectacular 提供,用來自動產生 API 文件(Swagger UI)。
  • LimitOffsetPagination:提供分頁功能,可透過 limitoffset 控制查詢結果。

功能說明

ArticleListView.get():提供 GET 請求,用來查詢文章列表,具備以下特性:

1. 查詢參數

  • limit:一次取回幾筆資料(預設 50)。
  • offset:從哪一筆資料開始(預設 0)。
  • author_name:依發文者名稱過濾。
  • board_name:依版面名稱過濾。
  • start_date, end_date:依發文時間範圍過濾(格式 YYYY-MM-DD)。

2. 輸入驗證

使用 ArticleListRequestSerializer 驗證查詢參數是否符合規格。

3. 文章過濾邏輯

根據提供的條件(作者、版面、時間區間)過濾資料庫的 Article 物件。

4. 時間處理

  • start_date 對應 00:00:00
  • end_date 對應 23:59:59
    使用 datetime.combine() 建立完整的 datetime 物件。

5. 分頁處理

使用 LimitOffsetPagination 回傳有分頁資訊的結果(count, next, previous, results)。

錯誤處理

若查詢參數格式錯誤,會紀錄錯誤 Log 並回傳 400 回應。

回傳格式範例

{
  "count": 0,
  "next": "string",
  "previous": "string",
  "results": [
    {
      "id": 0,
      "board": "string",
      "title": "string",
      "author": "string",
      "content": "string",
      "post_time": "2025-06-23T08:53:58.640Z",
      "url": "string"
    }
  ]
}

都完成後記得測試 API 喔!
API 完成的結果會類似以下範例:
https://ithelp.ithome.com.tw/upload/images/20250623/20172834Oq6eNux7Vo.png
https://ithelp.ithome.com.tw/upload/images/20250813/20172834akupBi0Oyn.png


明天【Day15】建立文章 API:ID查詢與統計
我們將會練習建立另外兩個API:
/api/posts/{id}/:查詢特定文章詳細內容。
/api/statistics/:查詢統計資訊(文章總數),可透過 時間範圍、版面、作者 過濾。


上一篇
【Day 13】Django REST Framework 入門介紹 - 打造高彈性 RESTful API 的利器
系列文
一起來打造 PTT 文章智慧問答系統!14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言